package ar.edu.itba.pod.legajo45002;

import java.rmi.AccessException;
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

import ar.edu.itba.balance.api.AgentsBalancer;
import ar.edu.itba.balance.api.AgentsTransfer;
import ar.edu.itba.balance.api.NodeAgent;
import ar.edu.itba.balance.api.NotCoordinatorException;
import ar.edu.itba.node.Node;
import ar.edu.itba.node.NodeInformation;
import ar.edu.itba.node.api.NodeStatistics;

@SuppressWarnings("serial")
public class AgentBalancerImpl extends UnicastRemoteObject implements AgentsBalancer {

	private NodeInformation nodeinfo;
	private AtomicBoolean shouldSendElection = new AtomicBoolean();
	private AtomicBoolean iAmCoordinator = new AtomicBoolean();
	private boolean shouldWait;
	private long electionTimestamp;;
	private Thread timeout;
	private long sleepTimeForCoordinator = 1000;

	private AtomicLong coordinatorTimestamp = new AtomicLong(-1);


	Map<NodeInformation, Long>nodesMap = new ConcurrentHashMap<NodeInformation, Long>();

	protected AgentBalancerImpl(NodeInformation info) throws RemoteException {
		super();
		nodeinfo = info;
		electionTimestamp = -1; 
		iAmCoordinator.set(false);
		shouldWait = false;
		Registry reg = DistributedNodeImpl.getRegistry();
		if( reg != null){
			try {
				System.out.println("registering balancer");
				reg.bind(Node.AGENTS_BALANCER, this);
			} catch (AlreadyBoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}



	}

	private void executeWaitingThread(){
		//	if ( shouldWait){
		shouldWait = false;
		iAmCoordinator.set(true);
		if(timeout != null &&  timeout.isAlive()){
			timeout.interrupt();
		}
		timeout = new Thread(){

			public void run() {
				try {
					System.out.println("Waiting");
					Thread.sleep(sleepTimeForCoordinator);
					if( iAmCoordinator.get()){
						//	System.out.println("sending i am coordinator to everyone");
						iamTheCoordinator(nodeinfo, System.nanoTime());
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				} catch (RemoteException e) {
					e.printStackTrace();
				}
			};
		};
		timeout.start();
		//	}
	}



	private void doElection(final NodeInformation node, final long timestamp){

		Thread electionThread = new Thread(){
			public void run() {

				for( NodeInformation aNode : DistributedNodeImpl.allNodes().values()){
					if( ! aNode.equals(nodeinfo) && !aNode.equals(node) ){
						System.out.println("sending bullyelection to node: "+ aNode.id());
						Registry registry = null;

						try {
							registry = LocateRegistry.getRegistry(aNode.host(),aNode.port());
						} catch (RemoteException e1) {
							e1.printStackTrace();
						}
						AgentsBalancer stub = null;
						try {
							stub = (AgentsBalancer)registry.lookup(Node.AGENTS_BALANCER);
						} catch (NotBoundException e) {
							e.printStackTrace();
						} catch (AccessException e) {
							e.printStackTrace();
						} catch (RemoteException e) {
							e.printStackTrace();
						}
						try {
							stub.bullyElection(node, timestamp);
						} catch (RemoteException e) {
							e.printStackTrace();
						}
					}
				}
			};
		};
		electionThread.start();
	}

	private void sendBullyOk(NodeInformation node) throws RemoteException{
		Registry registry = LocateRegistry.getRegistry(node.host(),node.port());
		AgentsBalancer stub = null;
		try {
			stub = (AgentsBalancer)registry.lookup(Node.AGENTS_BALANCER);
		} catch (NotBoundException e) {
			e.printStackTrace();
		}
		System.out.println("sendingBullyOk to node: " + node.id());
		stub.bullyOk(nodeinfo);
	}


	@Override
	public void bullyElection(NodeInformation node, long timestamp)	throws RemoteException {
		System.out.println("bullyElection" + timestamp +"   "+ electionTimestamp);
		//	if ( !node.equals(nodeinfo) ){
		if( nodesMap.containsKey(node) ){
			if( nodesMap.get(node).equals(timestamp)  ||  nodesMap.get(node) > timestamp){
				//nodo que me llamo y timestamp son iguales
				return;
			}else if( nodesMap.get(node)< timestamp){
				System.out.println( nodeinfo.id().compareTo(node.id())  );

				//nodo que me llamo ya lo tenia guardado y timestamp que me mando es mayor al de antes
				nodesMap.put(node, timestamp);
				if( nodeinfo.id().compareTo(node.id()) < 0 ){
					//Si el nodo que me mando el election, tiene id mas grande que el mio
					//hago forward del election al resto
					doElection(node, timestamp);
				}else if (nodeinfo.id().compareTo(node.id()) > 0 ){
					//Mando bullyok al nodo que me llamo
					sendBullyOk(node);
					//arranco una nueva eleccion con mi nodo y un nuevo timestamp
					doElection(nodeinfo, System.nanoTime());
					executeWaitingThread();
				}
			}
		}else {
			//No es mensaje repetido
			nodesMap.put(node, timestamp);
			if( nodeinfo.id().compareTo(node.id()) > 0){
				//Mando bullyok al nodo que me llamo
				sendBullyOk(node);
				//arranco una nueva eleccion con mi nodo y un nuevo timestamp
				doElection(nodeinfo, System.nanoTime());
				executeWaitingThread();
			}else if( nodeinfo.id().compareTo(node.id()) < 0 ){
				//Si el nodo que me mando el election, tiene id mas grande que el mio
				iAmCoordinator.set(false);
				System.out.println("node: " +node.id() +"is bigger than me");
				shouldWait = false;
				doElection(node, timestamp);
			}else{
				executeWaitingThread();
				doElection(nodeinfo, timestamp);
			}
		}
		//	}
	}


	public void iamTheCoordinator(NodeInformation me, long timestamp)throws RemoteException{
		DistributedNodeImpl.setCoordinator(nodeinfo);
		System.out.println("I AM THE COORDINATOR, I HAVE THE POWER");
		
		for( NodeInformation aNode : DistributedNodeImpl.allNodes().values()){

			if( !aNode.equals(nodeinfo)){
				System.out.println("sending iamTheCoordinator to node: " + aNode.id());

				Registry registry = LocateRegistry.getRegistry(aNode.host(),aNode.port());
				AgentsBalancer stub = null;
				try {
					stub = (AgentsBalancer)registry.lookup(Node.AGENTS_BALANCER);
				} catch (NotBoundException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				stub.bullyCoordinator(me, timestamp);
			}
		}
		try {
			DistributedNodeImpl.balanceAgents();
		} catch (NotBoundException e1) {
			e1.printStackTrace();
		}
	}

	@Override
	public void bullyOk(NodeInformation node) throws RemoteException {
		System.out.println("received bully OK from node" + node.id());
		shouldSendElection.set(false);
		iAmCoordinator.set(false);
	}

	@Override
	public void bullyCoordinator(NodeInformation node, long timestamp)throws RemoteException {
		//	Seteo como coordinaror a "node" en mi variable coordinator
		if( coordinatorTimestamp.get() == -1 || timestamp > coordinatorTimestamp.get() ){
			coordinatorTimestamp.set(timestamp);
			DistributedNodeImpl.setCoordinator(node);
			System.out.println("coordinator: " +node.id());
			if( timeout != null && timeout.isAlive())
				timeout.interrupt();
			for( NodeInformation n : DistributedNodeImpl.allNodes().values()){
				Registry registry = LocateRegistry.getRegistry(n.host(),n.port());
				AgentsBalancer stub = null;
				try {
					stub = (AgentsBalancer)registry.lookup(Node.AGENTS_BALANCER);
				} catch (NotBoundException e) {
					e.printStackTrace();
				}
				System.out.println("sending BullyCoordinator to node: " + n.id());
				stub.bullyCoordinator(node, timestamp);
			}
		}
	}

	@Override
	public void shutdown(List<NodeAgent> agents) throws RemoteException, NotCoordinatorException {

		if (! DistributedNodeImpl.getMynodeInfo().equals(DistributedNodeImpl.getCoordinator())){
			throw new NotCoordinatorException(DistributedNodeImpl.getCoordinator());
		}
		System.out.println("I am the coordinator, and will balance agents");
		try {
			DistributedNodeImpl.reOrganiceAgents(agents);
		} catch (NotBoundException e) {
			e.printStackTrace();
		}


	}

	@Override
	public void addAgentToCluster(NodeAgent agent) throws RemoteException,NotCoordinatorException {
		if(! DistributedNodeImpl.getMynodeInfo().equals(DistributedNodeImpl.getCoordinator())){
			throw new NotCoordinatorException(DistributedNodeImpl.getCoordinator());
		}
		System.out.println("Adding agent to cluster...");

		NodeInformation nodeToBalance = null;
		int agents = 10000;

		for( NodeInformation aNode: DistributedNodeImpl.allNodes().values()){
			int agentsInNode = 0;
			if( aNode.equals(DistributedNodeImpl.getMynodeInfo())){
				agentsInNode = DistributedNodeImpl.getSimulation().agentsRunning();
			}else{
				Registry registry = LocateRegistry.getRegistry(aNode.host(),aNode.port());

				AgentsTransfer stub = null;
				try {
					stub = (AgentsTransfer)registry.lookup(Node.AGENTS_TRANSFER);
				} catch (NotBoundException e) {
					e.printStackTrace();
				}
				agentsInNode =stub.getNumberOfAgents();
			}
			if( agentsInNode < agents){
				agents = agentsInNode;
				nodeToBalance = aNode;
			}
		}
		AgentsTransfer stub = null;

		Registry registry = LocateRegistry.getRegistry(nodeToBalance.host(),nodeToBalance.port());
		try {
			stub = (AgentsTransfer)registry.lookup(Node.AGENTS_TRANSFER);
		} catch (NotBoundException e) {
			e.printStackTrace();
		}
		LinkedList<NodeAgent> nodeTuRun = new LinkedList<NodeAgent>();
		nodeTuRun.add(agent);
		System.out.println("sending agent to node: "+ nodeToBalance.id());
		stub.runAgentsOnNode( nodeTuRun);

		/*
		Collections.sort(orderdNodesInformations, new Comparator<NodeInformation>() {

			@Override
			public int compare(NodeInformation o1, NodeInformation o2) {

				return 0;
			}
		});
		 */
	}

}
